package com.informatica.test.gwt.server;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

import com.informatica.test.gwt.server.UserContext;
import com.informatica.test.gwt.server.Configuration;

public class BaseDelegate {
    enum HttpMethod {
        GET, POST, PUT, DELETE
    }

    private Configuration configuration;
    private ObjectMapper objectMapper;
    private UserContext context;

    public BaseDelegate(Configuration configuration, ObjectMapper om, UserContext context) {
        this.configuration = configuration;
        this.objectMapper = om;
        this.context = context;
    }

    public final JsonNode get(String url, Object... args) {
        return excuteRequest(HttpMethod.GET, url, args, null);
    }

    public final JsonNode post(String url, Object... args) {
        return excuteRequest(HttpMethod.POST, url, args, null);
    }

    public final JsonNode post(String url, JsonNode request, Object... args) {
        StringWriter writer = jsonToString(request);
        return excuteRequest(HttpMethod.POST, url, args, writer.toString());
    }

    protected final void delete(String url, Object... args) {
        excuteRequest(HttpMethod.DELETE, url, args, null);
    }

    protected StringWriter jsonToString(JsonNode request) {
        try {
            StringWriter writer = new StringWriter();
            objectMapper.writeValue(writer, request);
            return writer;
        } catch (JsonGenerationException e) {
            throw new RuntimeException("Cannot serialize json object", e);
        } catch (JsonMappingException e) {
            throw new RuntimeException("Cannot serialize json object", e);
        } catch (IOException e) {
            throw new RuntimeException("Cannot serialize json object", e);
        }
    }

    protected String constructUrl(String url, Object args[]) {
        StringBuilder sb = new StringBuilder(configuration.getBaseUrl());
        if (sb.charAt(sb.length() - 1) != '/') {
            sb.append('/');
        }
        sb.append("api");
        if (url.charAt(0) != '/') {
            sb.append('/');
        }
        sb.append(url);

        if (args == null) {
            return sb.toString();
        }

        for (int i = 0; i < args.length; i++) {
            int index = -1;
            String argName = "{" + i + "}";
            while ((index = sb.indexOf(argName)) != -1) {
                sb.replace(index, index + argName.length(), String.valueOf(args[i]));
            }
        }

        return sb.toString();
    }

    private JsonNode excuteRequest(HttpMethod type, String targetURL, Object args[], String body) {
        URL url;
        HttpURLConnection connection = null;
        try {
            // Create connection
            url = new URL(constructUrl(targetURL, args));
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod(type.name());
            connection.setRequestProperty("Accept", "application/json");
            connection.setRequestProperty("Content-Type", "application/json");
            if (body != null) {
                connection.setRequestProperty("Content-Length", "" + Integer.toString(body.getBytes().length));
            }

            if (context.getSessionId() != null) {
                connection.setRequestProperty("Cookie", "JSESSIONID=" + context.getSessionId() + ";");
            }

            connection.setUseCaches(false);
            connection.setDoInput(true);
            connection.setDoOutput(true);

            // Send request
            if (body != null) {
                DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
                wr.writeBytes(body);
                wr.flush();
                wr.close();
            } else {
                connection.connect();
            }

            boolean isOk = (connection.getResponseCode() / 100 == 2);

            if (!isOk) {
                InputStream errStream = connection.getErrorStream();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                copyStreamNotClose(errStream, baos);
                System.out.println("error:" + new String(baos.toByteArray()));
                JsonNode node = objectMapper.readTree(new ByteArrayInputStream(baos.toByteArray()));
                String msg = node.get("description").asText();   /////////////////////////////
                throw new RuntimeException(msg);
            }
            // Get Response

            String cookie = connection.getHeaderField("Set-Cookie");
            if (cookie != null) {
                cookie = cookie.substring(0, cookie.indexOf(';'));
                context.setSessionId(cookie.split("=", 2)[1]); // XXX danger
                                                               // code
                System.out.println("update cookie: " + cookie);
            }

            InputStream is = connection.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            copyStreamNotClose(is, baos);
            byte[] byteArray = baos.toByteArray();
            System.out.println("received:" + new String(byteArray));
            if (byteArray.length == 0) {
                return null;
            }
            JsonNode node = objectMapper.readTree(new ByteArrayInputStream(byteArray));
            is.close();
            return node;
        } catch (IOException e) {
            throw new RuntimeException("Error during request execution", e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    public static void copyStreamNotClose(InputStream in, OutputStream out) throws IOException {
        int len;
        byte[] buf = new byte[8192];
        while ((len = in.read(buf)) > 0) {
            out.write(buf, 0, len);
        }
    }

    /**
     * Close stream safely, no exception will be thrown
     * 
     * @param stream
     *            the stream
     */
    public static void closeStream(Closeable stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
            }
        }
    }
}

